import numpy as np
import matplotlib.pyplot as plt




# 实现求函数的导数的程序
# def numerial_diff(f,x):
#     h = 10e-50
#     return (f(x+h)-f(x))/h
# 函数 numerical_diff(f,x) 的名称来源于数值微分.乍一看这个实现没有问题,但是实际上这段代码有两处需要改进的地方。
# 1、10e-50这个数值太小了，会导致舍入误差
# 2、虽然上述实现中计算了函数 f 在 x+h 和 x 之间的差分,但是必须注意到,这个计算从一开始就有误差。
# “真的导数”对应函数在 x 处的斜率(称为切线),但上述实现中计算的导数对应的是 (x + h) 和 x 之间的斜率。因此,真的导数(真的切线)
# 和上述实现中得到的导数的值在严格意义上并不一致。这个差异的出现是因为 h 不可能无限接近 0。
# 数值微分含有误差。为了减小这个误差,我们可以计算函数 f 在 (x + h) 和 (x − h) 之间的差分。因为这种计算方法以 x 为中心,计
# 算它左右两边的差分,所以也称为中心差分 (而 (x + h) 和 x 之间的差分称为前向差分)。

# 改进的代码：
def numerical_diff(f,x):
    h = 1e-4
    return (f(x+h)-f(x-h))/(2*h)


def function_1(x):
    return 0.01*x**2 + 0.1*x

x = np.arange(0.0,20.0,0.1)
y = function_1(x)
# plt.xlabel('x')
# plt.ylabel('f(x)')
# plt.plot(x,y)
# plt.show()

# print(numerical_diff(function_1,5))
# 输出结果为0.1999999999990898

# 偏导数
def function_2(x):
    return x[0]**2+x[1]**2

# 求 x0 = 3, x1 = 4 时,关于 x0 的偏导数
def function_tmp1(x0):
    return x0*x0+4.0**2.0

# print(numerical_diff(function_tmp1,3))
# 输出结果为6.00000000000378

# 求 x0 = 3, x1 = 4 时,关于 x1 的偏导数
def function_tmp2(x1):
    return 3.0**2.0+x1*x1

# print(numerical_diff(function_tmp2,4))
# 输出结果为7.999999999999119


# 由全部变量的偏导数汇总而成的向量称为梯度 (gradient)
def numerical_gradient(f,x):
    h = 1e-4
    grad = np.zeros_like(x)
    for idx in range(x.size): # 例如输入的参数是x0,x1,则先求(f(x0+h,x1)-f(x0-h,x1))/(2*h)
        tmp_val = x[idx]
        # 计算f(x+h)
        x[idx] = tmp_val + h
        fxh1 = f(x)
        # 计算f(x-h)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        # 计算grad并还原x[idx]
        grad[idx] = (fxh1 - fxh2)/(2*h)
        x[idx] = tmp_val
    
    return grad


# print(numerical_gradient(function_2,np.array([3.0,4.0])))
# 输出结果为[6. 8.]，即点（3，4）处的梯度为（6，8)

# 机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必
# 须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数
# 取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我
# 们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值
# (或者尽可能小的值)的方法就是梯度法。
# 这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,
# 无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际
# 上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
# 虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地
# 减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的
# 任务中,要以梯度的信息为线索,决定前进的方向。
# 此时梯度法就派上用场了。在梯度法中,函数的取值从当前位置沿着梯
# 度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,
# 如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,
# 逐渐减小函数值的过程就是梯度法 (gradient method)。梯度法是解决机器
# 学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。

def gradient_descent(f,init_x,lr=0.01,step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr*grad
    return x

# init_x = np.array([-3.0,4.0])
# print(gradient_descent(function_2,init_x,lr = 0.1,step_num=100))

